package globalVisionEntertainment.nba;

import gqqnbig.StringHelper;
import gqqnbig.Tuple;
import gqqnbig.jsoup.AdaptiveDownloader;

import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

public class EspnGameRecord
{
	private static final AdaptiveDownloader downloader = new AdaptiveDownloader();

	/**
	 * A list of regular expressions that if a text matches one of them, the player is on court. Get the name of the
	 * player by named group called "n".
	 */
	static final String[] playerOnCourtIndicators;

	/**
	 * m:ss
	 */
	private final static SimpleDateFormat dateFormat;

	private final static int timeoutRetry = 3;

	static
	{
		List<String> indicators = new ArrayList<String>();

		// The regular expression should have a named group called n,
		// containing the player's name.

		// http://espn.go.com/nba/playbyplay?gameId=400489940&period=4
		// 有Dwyane Wade makes dunk (LeBron James assists)，在匹配名称的部分要排除括号。
		indicators.add("(?<n>[^\\(\\)]+) makes .+? point shot");

		// http://espn.go.com/nba/playbyplay?gameId=300310020&period=0
		// indicators.add("(?<n>[^\\(\\)]+) defensive rebound"); //篮板球。有时候直接说Charlotte offensive rebound
		indicators.add("(?<n>[^\\(\\)]+) bad pass");

		// http://espn.go.com/nba/playbyplay?gameId=320204024&period=3
		// double personal foul: Kawhi Leonard (2) and Kevin Durant (4)
		// are each charged with a personal foul
		indicators.add("^(?<n>[^\\(\\)]+(?<!double)) personal foul"); // 不能以double结尾。
		// indicators.add("(?<n>[^\\(\\)]+) (personal|shooting) (foul|block)");
		indicators.add("(?<n>[^\\(\\)]+) (makes|misses) free throw");
		indicators.add("(?<n>[^\\(\\)]+) (makes|misses) .*?jumper");
		indicators.add("(?<n>[^\\(\\)]+) (makes|misses) layup");

		indicators.add("(?<n>[^\\(\\)]+) assists");

		indicators.add("blocks (?<n>.+)'s layup");

		indicators.add("(?<n>[^\\(\\)]+) makes dunk");
		indicators.add("(?<n>[^\\(\\)]+) defensive goaltending violation");
		indicators.add("(?<n>[^\\(\\)]+) traveling");

		playerOnCourtIndicators = new String[indicators.size()];
		indicators.toArray(EspnGameRecord.playerOnCourtIndicators);

		dateFormat = new SimpleDateFormat("m:ss");
		dateFormat.setTimeZone(new SimpleTimeZone(0, "UTC"));
	}

	static void readPlayByPlayOnCourtPeriod(Element playByPlayTable, List<PlayerInQuarterStatistic> playerStatistics, GameParty party)
	{
		Elements changingRowsTd;

		// http://espn.go.com/nba/playbyplay?gameId=320617014&period=0
		// 客队在左，主队在右。
		if (party == GameParty.Visitor)
			changingRowsTd = playByPlayTable.select("tr>td:eq(1):contains(enters the game for)");
		else
			changingRowsTd = playByPlayTable.select("tr>td:eq(3):contains(enters the game for)");
		Elements changingRows = new Elements(changingRowsTd.size());

		for (Element td : changingRowsTd)
			changingRows.add(td.parent());

		try
		{
			for (Element row : changingRows)
			{
				Date time = dateFormat.parse(row.select("td:eq(0)").first().text());

				String[] parts;
				if (party == GameParty.Visitor)
					parts = row.select("td:eq(1)").first().text().split(" enters the game for ");
				else
					parts = row.select("td:eq(3)").first().text().split(" enters the game for ");

				String newPlayer = parts[0];
				String oldPlayer = parts[1];

				PlayerInQuarterStatistic oldPlayerStatistic = Program.getOnCourtPlayers(oldPlayer, playerStatistics);
				oldPlayerStatistic.setEndTime(time);

				playerStatistics.add(new PlayerInQuarterStatistic(newPlayer, time));
			}
		}
		catch (ParseException e)
		{
			throw new RuntimeException(e);
		}

		Date endTime;
		//@formatter:off
		try	{ endTime = dateFormat.parse("0:00");	} 
		catch (ParseException e) { throw new RuntimeException(e);}
		//@formatter:on

		assert endTime.getTime() == 0 : "May not using UTC time.";
		for (PlayerInQuarterStatistic playerStatistic : playerStatistics)
		{
			if (playerStatistic.getEndTime() == null)
				playerStatistic.setEndTime(endTime);
		}

	}

	public static Document parseWebDocument(String url) throws IOException
	{

		return downloader.parseWebDocument(url, timeoutRetry);
	}

	private final String gameId;

	/**
	 * start accumulated scores of each section. The last element is the total score of the first four sections.
	 */
	GamePartiesData<Integer>[] initialScores;

	GamePartiesData<Map<String, String>> starterPositions = new GamePartiesData<Map<String, String>>();
	GamePartiesData<Map<String, String>> benchPositions = new GamePartiesData<Map<String, String>>();

	GamePartiesData<Map<String, Integer>> starterVirtualPosition = new GamePartiesData<Map<String, Integer>>();

	// GamePartiesData<Set<String>> partyPlayers = new GamePartiesData();

	List<Document> sectionDocuments = new ArrayList<Document>(BasketballRules.normalSectionCount);

	private Game game;

	public EspnGameRecord(String gameId) throws IOException
	{
		this.gameId = gameId;

		for (int i = 0; i < BasketballRules.normalSectionCount; i++)
			sectionDocuments.add(null);

		readPlayerStarterAndPosition();
	}

	public String getGameId()
	{
		return gameId;
	}

	/**
	 * 
	 * @param playByPlayTable
	 * @param section
	 *            Start from 1.
	 * @param time
	 * @return [0]: score of home team; [1]: score of visiting team.
	 */
	int[] findScoresByTime(Element playByPlayTable, int section, Date time)
	{
		String timeString = dateFormat.format(time);
		if (timeString.equals(BasketballRules.sectionLength + ":00"))
			return new int[] { initialScores[section - 1].homeTeam, initialScores[section - 1].visitingTeam };
		else if (timeString.equals("0:00"))
			return new int[] { initialScores[section].homeTeam, initialScores[section].visitingTeam };

		Elements tds = playByPlayTable.select("tr>td:eq(0):matches(^" + timeString + "$)");

		Element tr = null;
		for (int i = 0; i < tds.size(); i++)
		{
			tr = tds.get(i).parent();
			if (tr.children().size() == 4)
				break;
		}

		assert tr != null : "Cannot find a row matching the time of player changing.";

		String[] parts = tr.select("td:eq(2)").text().split("-");
		int[] scores = new int[2];
		scores[0] = Integer.parseInt(parts[0]);
		scores[1] = Integer.parseInt(parts[1]);

		return scores;
	}

	List<PlayerQuarterStatistic> readPlayByPlayPlusMinus(Element playByPlayTable, GameParty party, int quarter, List<PlayerInQuarterStatistic> playerStatistics)
	{
		for (PlayerInQuarterStatistic p : playerStatistics)
		{
			int[] startScores = findScoresByTime(playByPlayTable, quarter, p.getStartTime());
			int[] endScores = findScoresByTime(playByPlayTable, quarter, p.getEndTime());

			int plus;
			int minus;
			if (party == GameParty.Home)
			{
				plus = endScores[0] - startScores[0];
				minus = endScores[1] - startScores[1];
			}
			else
			{
				plus = endScores[1] - startScores[1];
				minus = endScores[0] - startScores[0];
			}
			assert plus >= 0;
			assert minus >= 0;

			p.setPlus(plus);
			p.setMinus(minus);
		}

		List<PlayerQuarterStatistic> quarterStat = Program.coalesceByName(playerStatistics);

		return quarterStat;

	}

	public List<PlayerQuarterStatistic> getQuarterStatistic(int quarter, GameParty party) throws IOException
	{
		System.out.println("\nSection " + quarter + " starts.");

		Document playByplayPage;
		if (BuildMode.IsDebug)
		{
			String fileName = String.format("playbyplay%s.html", quarter);
			playByplayPage = Jsoup.parse(new File(fileName), "utf8");
		}
		else
			playByplayPage = getSectionDocument(quarter, String.format("http://espn.go.com/nba/playbyplay?gameId=%s&period=%s", gameId, quarter));

		if (initialScores == null)
			getInitialScores(playByplayPage);

		ArrayList<PlayerInQuarterStatistic> playerStatistics = new ArrayList<PlayerInQuarterStatistic>();

		Element playByPlayTable = playByplayPage.select("div.mod-content>table.mod-data>tbody").first();

		if (playByPlayTable == null)
			throw new UnsupportedOperationException("Unable to find play by play table. Probably Play-By-Play is not available.");

		readPlayByPlayOnCourtPeriod(playByPlayTable, playerStatistics, party);
		for (PlayerInQuarterStatistic p : playerStatistics)
			System.out.printf("%s\t played from %s to %s \n", p.getPlayerName(), dateFormat.format(p.getStartTime()), dateFormat.format(p.getEndTime()));

		int onCourtPlayerCount = Program.isEnoughPlayersOnCourt(playerStatistics);
		if (onCourtPlayerCount < BasketballRules.maximunOnCourtPlayers)
			addFullSectionPlayersHeuristically(playByPlayTable, party, BasketballRules.maximunOnCourtPlayers - onCourtPlayerCount, playerStatistics);

		List<PlayerQuarterStatistic> quarterStat = readPlayByPlayPlusMinus(playByPlayTable, party, quarter, playerStatistics);

		return quarterStat;
	}

	/**
	 * 
	 * @param section
	 *            starts from 1.
	 * @param url
	 *            TODO
	 * @return
	 * @throws IOException
	 */
	private Document getSectionDocument(int section, String url) throws IOException
	{
		if (sectionDocuments.get(section - 1) != null)
			return sectionDocuments.get(section - 1);

		Document document = parseWebDocument(url);
		sectionDocuments.set(section - 1, document);
		return document;
	}

	/**
	 * utilize <code>playerOnCourtIndicators</code> to find the players that were not substituted.
	 * 
	 * @param playByPlayTable
	 * @param party
	 * 
	 * @param addCount
	 *            how many players should this method find
	 * @param playerStatistics
	 */
	private void addFullSectionPlayersHeuristically(Element playByPlayTable, GameParty party, int addCount, ArrayList<PlayerInQuarterStatistic> playerStatistics)
	{
		System.out.println("Heuristically start to find full section players: need " + addCount + " more players.");

		Date startDate;
		Date endDate;
		try
		{
			startDate = dateFormat.parse("12:00");
			endDate = dateFormat.parse("0:00");
		}
		catch (ParseException e)
		{
			throw new RuntimeException(e);
		}

		Elements tds;
		if (party == GameParty.Visitor)
			tds = playByPlayTable.select("td:eq(1):contains( )");
		else
			tds = playByPlayTable.select("td:eq(3):contains( )");

		for (String pattern : playerOnCourtIndicators)
		{
			Pattern p = Pattern.compile(pattern);
			for (Element td : tds)
			{
				Matcher matcher = p.matcher(td.text());
				if (matcher.find())
				{
					String playerName = matcher.group("n");

					int i = 0;
					for (; i < playerStatistics.size(); i++)
					{
						if (playerStatistics.get(i).getPlayerName().equals(playerName))
							break;
					}

					if (i == playerStatistics.size())
					{
						System.out.printf("found %s by pattern \"%s\"\n", playerName, p.pattern());

						PlayerInQuarterStatistic ps = new PlayerInQuarterStatistic(playerName, startDate);
						ps.setEndTime(endDate);
						playerStatistics.add(ps);

						addCount--;
						if (addCount == 0)
							return;
					}
				}
			}
		}

		assert addCount == 0 : new UnsupportedOperationException(String.format("Still miss %s players after heuristical search.", addCount));
		return;
	}

	private void getInitialScores(Document playByplayPage)
	{
		assert initialScores == null;

		// last element for total scores.
		initialScores = new GamePartiesData[BasketballRules.normalSectionCount + 1];
		for (int i = 0; i < initialScores.length; i++)
			initialScores[i] = new GamePartiesData<Integer>();
		initialScores[0].homeTeam = 0;
		initialScores[0].visitingTeam = 0;

		Elements tds = playByplayPage.select("table.linescore tr:eq(1)>td:gt(0):lt(5)");
		int accumulateScore = 0;
		for (int i = 0; i < tds.size(); i++)
		{
			accumulateScore += Integer.parseInt(tds.get(i).text());
			initialScores[i + 1].homeTeam = accumulateScore;
		}

		tds = playByplayPage.select("table.linescore tr:eq(2)>td:gt(0):lt(5)");
		accumulateScore = 0;
		for (int i = 0; i < tds.size(); i++)
		{
			accumulateScore += Integer.parseInt(tds.get(i).text());
			initialScores[i + 1].visitingTeam = accumulateScore;
		}
	}

	public PlayerGameStatistic[] readGameStatistic(GameParty party) throws IOException
	{

		Map<String, String> starterPositions = this.starterPositions.get(party);
		Map<String, String> benchPositions = this.benchPositions.get(party);
		Map<String, Integer> starterVirtualPosition = this.starterVirtualPosition.get(party);
		HashMap<String, String> nickNameTranslator = new HashMap<String, String>();

		ArrayList<PlayerGameStatistic> playerGameStatistics = new ArrayList<PlayerGameStatistic>();
		for (int i = 1; i <= BasketballRules.normalSectionCount; i++)
		{
			List<PlayerQuarterStatistic> quarterStatistic = getQuarterStatistic(i, party);

			for (int j = 0; j < quarterStatistic.size(); j++)
			{
				PlayerQuarterStatistic pqs = quarterStatistic.get(j);
				if (pqs.getOnCourtTime().getTime() / 1000 > BasketballRules.sectionLength * 60)
				{
					String message = String.format("%s played over %s minutes, which is impossible.", pqs.getPlayerName(), pqs.getOnCourtTime().getTime() / 1000 / 60);
					throw new UnsupportedOperationException(message);
				}

				String playerName = pqs.getPlayerName();
				if (nickNameTranslator.containsKey(playerName))
					playerName = nickNameTranslator.get(playerName);

				PlayerGameStatistic target = null;
				if ((target = findPlayerGameStatistic(playerGameStatistics, playerName)) != null)
					target.getPlayerGameStatistic()[i - 1] = pqs;
				else
				{
					boolean isStarter = starterPositions.containsKey(playerName);
					if (isStarter == false && benchPositions.containsKey(playerName) == false)
					{
						// http://espn.go.com/nba/playbyplay?gameId=400556176
						// Play-by-play说Nene，Box Score说Nene Hilario。名称不统一会导致上述断言失败。
						// 也有例子http://espn.go.com/nba/boxscore?id=320606025，Danny Green和Daniel Green。
						// 不知道是否还有其他球员的例子，先不处理。

						HashSet<String> union = new HashSet<String>(starterPositions.keySet());
						union.addAll(benchPositions.keySet());
						playerName = findPlayerReadName(playerName, union);

						System.out.println(pqs.getPlayerName() + "'s real name is " + playerName + ".");
						nickNameTranslator.put(pqs.getPlayerName(), playerName);
						j--;// 重新处理此元素。

					}
					else
					{

						String position;
						if (isStarter)
							position = starterPositions.get(playerName);
						else
							position = benchPositions.get(playerName);

						int virtualPosition;
						if (isStarter)
							virtualPosition = starterVirtualPosition.get(playerName);
						else
							virtualPosition = 0;

						target = new PlayerGameStatistic(playerName, isStarter, position, virtualPosition);

						target.getPlayerGameStatistic()[i - 1] = pqs;

						playerGameStatistics.add(target);
					}
				}
			}
		}

		// if (party == GameParty.Home)
		// {
		// partyPlayers.homeTeam = new HashSet<String>(map.keySet());
		//
		// if (partyPlayers.visitingTeam != null)
		// {
		// Set<String> union = new HashSet<String>(partyPlayers.homeTeam);
		// union.retainAll(partyPlayers.visitingTeam);
		// if (union.size() > 0)
		// throw new RuntimeException("Home team and visiting team have the same players! " + union.toString());
		// }
		// }
		// else
		// {
		// partyPlayers.visitingTeam = new HashSet<String>(map.keySet());
		// if (partyPlayers.homeTeam != null)
		// {
		// Set<String> union = new HashSet<String>(partyPlayers.homeTeam);
		// union.retainAll(partyPlayers.visitingTeam);
		// if (union.size() > 0)
		// throw new RuntimeException("Home team and visiting team have the same players! " + union.toString());
		// }
		// }

		PlayerGameStatistic[] result = new PlayerGameStatistic[playerGameStatistics.size()];
		playerGameStatistics.toArray(result);

		return result;
	}

	private static PlayerGameStatistic findPlayerGameStatistic(Iterable<PlayerGameStatistic> list, String playerName)
	{
		for (PlayerGameStatistic value : list)
		{
			if (value.getPlayerName().equals(playerName))
				return value;
		}
		return null;
	}

	private static String findPlayerReadName(String nickName, Set<String> nameSet)
	{
		// http://espn.go.com/nba/boxscore?id=310412013
		// 无法关联Metta World Peace与Ron Artest。

		ArrayList<String> filteredNames = new ArrayList<String>();
		String[] parts = nickName.split(" ");
		for (String n : nameSet)
		{
			int i = 0;
			for (; i < parts.length; i++)
			{
				if (n.contains(parts[i]))
					break;
			}
			if (i < parts.length)
				filteredNames.add(n);
		}

		if (filteredNames.size() == 1)
			return filteredNames.get(0);
		else if (filteredNames.size() == 0)
			throw new UnsupportedOperationException("Unable to find " + nickName + "'s read name");
		Tuple<String, Integer>[] distances = new Tuple[filteredNames.size()];
		for (int i = 0; i < filteredNames.size(); i++)
		{
			int d = StringHelper.getStringDistance(nickName, filteredNames.get(i));
			distances[i] = new Tuple<String, Integer>(filteredNames.get(i), d);
		}

		Arrays.sort(distances, new Comparator<Tuple<String, Integer>>()
		{
			@Override
			public int compare(Tuple<String, Integer> o1, Tuple<String, Integer> o2)
			{
				return o1.item2 - o2.item2;
			}
		});

		return distances[0].item1;

	}

	/**
	 * 可以多次调用，返回值在类内部缓存。
	 * 
	 * @return
	 * @throws Exception
	 */
	public Game getGameInfo() throws Exception
	{
		if (game != null)
			return game;

		Document document = getSectionDocument(1, String.format("http://espn.go.com/nba/playbyplay?gameId=%s&period=%s", gameId, 1));
		Elements nameElements = document.select("div.matchup>div.team .team-info>h3");

		game = new Game();
		game.id = gameId;

		if (nameElements.size() != 2)
			throw new Exception("Unable to find team information.");

		Element t = nameElements.get(1).nextElementSibling();
		if (t == null || t.text().contains("home") == false)
			System.err.println("Unable to distinguish visitor team name and home team name. Assign first to visitor and second to home.");
		game.homeTeam = getTeamName(nameElements.get(1));

		t = nameElements.get(0).nextElementSibling();
		if (t == null || t.text().contains("away") == false)
			System.err.println("Unable to distinguish visitor team name and home team name. Assign first to visitor and second to home.");
		game.visitingTeam = getTeamName(nameElements.get(0));

		Element dateElement = document.select(".game-vitals>.game-time-location>p:eq(0)").first();

		String dateString = dateElement.text();
		dateString = dateString.substring(dateString.indexOf(", ") + 2);

		SimpleDateFormat dateFormat = new SimpleDateFormat("MMMM d, y", Locale.US);
		try
		{
			game.date = dateFormat.parse(dateString);
			return game;
		}
		catch (ParseException e)
		{
			throw new RuntimeException(e);
		}
	}

	private String getTeamName(Element teamInfoElement)
	{
		// http://espn.go.com/nba/playbyplay?gameId=400517994
		// all stars don't have <a>.

		if (teamInfoElement.ownText().trim().length() == 0)
		{
			teamInfoElement = teamInfoElement.select("a").first();
			return teamInfoElement.text();
		}
		else
			return teamInfoElement.ownText().trim();
	}

	private void readPlayerStarterAndPosition() throws IOException
	{
		Document boxscoreDocument = parseWebDocument(String.format("http://espn.go.com/nba/boxscore?id=%s", gameId));

		// http://espn.go.com/nba/boxscore?gameId=320617014
		// 先客队，再主队。
		starterPositions.homeTeam = new HashMap<String, String>();
		starterVirtualPosition.homeTeam = new HashMap<String, Integer>();

		starterPositions.visitingTeam = new HashMap<String, String>();
		starterVirtualPosition.visitingTeam = new HashMap<String, Integer>();

		savePlayerPositionsTo(boxscoreDocument.select("#my-players-table > div > table > tbody:nth-child(2) > tr> td:nth-child(1)"), starterPositions.visitingTeam,
				starterVirtualPosition.visitingTeam);

		savePlayerPositionsTo(boxscoreDocument.select("#my-players-table > div > table > tbody:nth-child(8) > tr> td:nth-child(1)"), starterPositions.homeTeam,
				starterVirtualPosition.homeTeam);

		benchPositions.homeTeam = new HashMap<String, String>();
		benchPositions.visitingTeam = new HashMap<String, String>();
		savePlayerPositionsTo(boxscoreDocument.select("#my-players-table > div > table > tbody:nth-child(4) > tr> td:nth-child(1)"), benchPositions.visitingTeam, null);

		savePlayerPositionsTo(boxscoreDocument.select("#my-players-table > div > table > tbody:nth-child(10) > tr> td:nth-child(1)"), benchPositions.homeTeam, null);
	}

	private static void savePlayerPositionsTo(Elements starterPositionsElement, Map<String, String> map, Map<String, Integer> virtualPosition)
	{
		int i = 0;

		for (Element element : starterPositionsElement)
		{
			String playerName = element.select("a").first().text();
			String position = element.textNodes().get(0).text().substring(2);

			map.put(playerName, position);

			if (virtualPosition != null)
				virtualPosition.put(playerName, i++);
		}

	}

}
